package org.spiderflow.core.expression.parsing.ast.access;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.spiderflow.core.exception.TemplateException;
import org.spiderflow.core.expression.ExpressionError;
import org.spiderflow.core.expression.ExpressionTemplate;
import org.spiderflow.core.expression.ExpressionTemplateContext;
import org.spiderflow.core.expression.reflection.JavaReflection;
import org.spiderflow.core.expression.parsing.Span;
import org.spiderflow.core.expression.parsing.ast.Expression;
import org.spiderflow.core.expression.parsing.ast.call.MethodCall;

import java.io.IOException;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;

/**
 * Represents access of a member (field or method or entry in a map) of the form <code>object.member</code>. Members may
 * only be read from.
 *
 * @author zsh
 */
public class MemberAccess extends Expression {
    private static final String LENGTH_FLAG = "length";
    private final Expression object;
    private final Span name;
    private Object cachedMember;

    public MemberAccess(Expression object, Span name) {
        super(name);
        this.object = object;
        this.name = name;
    }

    /**
     * Returns the object on which to access the member.
     **/
    public Expression getObject() {
        return object;
    }

    /**
     * The name of the member.
     **/
    public Span getName() {
        return name;
    }

    /**
     * Returns the cached member descriptor as returned by {@link JavaReflection#getField(Object, String)} or
     * {@link JavaReflection#getMethod(Object, String, Object...)}. See {@link #setCachedMember(Object)}.
     **/
    public Object getCachedMember() {
        return cachedMember;
    }

    /**
     * Sets the member descriptor as returned by {@link JavaReflection#getField(Object, String)} or
     * {@link JavaReflection#getMethod(Object, String, Object...)} for faster member lookups. Called by {@link org.spiderflow.core.expression.interpreter.AstInterpreter} the
     * first time this node is evaluated. Subsequent evaluations can use the cached descriptor, avoiding a costly reflective
     * lookup.
     **/
    public void setCachedMember(Object cachedMember) {
        this.cachedMember = cachedMember;
    }

    @Override
    public Object evaluate(ExpressionTemplate template, ExpressionTemplateContext context) throws IOException {
        Object evaluate = getObject().evaluate(template, context);
        if (evaluate == null) {
            return null;
        }
        Object res = tryGetMapValue(evaluate);
        if (isSuccess(res)) {
            return res;
        }
        res = tryGetLengthByArray(evaluate);
        if (isSuccess(res)) {
            return res;
        }
        res = tryGetFieldByCache(evaluate);
        if (isSuccess(res)) {
            return res;
        }
        String text = getName().getText();
        res = tryGetFieldByReflection(evaluate, text);
        if (isSuccess(res)) {
            return res;
        }
        return tryCallMethod(template, context, evaluate);
    }

    private Object tryCallMethod(ExpressionTemplate template, ExpressionTemplateContext context, Object evaluate) throws IOException {
        String methodName = buildMethodNameByText(getName().getText());
        Object res = tryCallGetMethod(template, context, methodName, evaluate);
        if (isSuccess(res)) {
            return res;
        }
        return tryCallIsMethod(template, context, methodName, evaluate);
    }

    private Object tryCallGetMethod(ExpressionTemplate template, ExpressionTemplateContext context,
                                    String methodName, Object evaluate) throws IOException {
        MemberAccess access = new MemberAccess(this.object, new Span("get" + methodName));
        MethodCall methodCall = new MethodCall(getName(), access, Collections.emptyList());
        try {
            return methodCall.evaluate(template, context);
        } catch (TemplateException e) {
            if (ExceptionUtils.indexOfThrowable(e, InvocationTargetException.class) > -1) {
                throw ExpressionError.error(String.format("在%s中调用方法get%s发生异常"
                        , evaluate.getClass(), methodName), getSpan(), e);
            }
            return FAIL_FLAG;
        }
    }

    private Object tryCallIsMethod(ExpressionTemplate template, ExpressionTemplateContext context,
                                   String methodName, Object evaluate) throws IOException {
        try {
            MemberAccess access = new MemberAccess(this.object, new Span("is" + methodName));
            MethodCall methodCall = new MethodCall(getName(), access, Collections.emptyList());
            return methodCall.evaluate(template, context);
        } catch (TemplateException e) {
            if (ExceptionUtils.indexOfThrowable(e, InvocationTargetException.class) > -1) {
                throw ExpressionError.error(String.format("在%s中调用方法is%s发生异常"
                        , evaluate.getClass()
                        , methodName), getSpan(), e);
            }
            throw ExpressionError.error(String.format("在%s中找不到属性%s或者方法get%s、方法is%s"
                    , evaluate.getClass()
                    , getName().getText()
                    , methodName
                    , methodName), getSpan());
        }
    }

    private Object tryGetLengthByArray(Object evaluate) {
        if (evaluate.getClass().isArray() && Objects.equals(LENGTH_FLAG, getName().getText())) {
            // special case for array.length
            return Array.getLength(evaluate);
        }
        return FAIL_FLAG;
    }

    @SuppressWarnings("rawtypes")
    private Object tryGetMapValue(Object evaluate) {
        if (evaluate instanceof Map) {
            // special case for map, allows map.key instead of map[key]
            Map map = (Map) evaluate;
            return map.get(getName().getText());
        }
        return FAIL_FLAG;
    }

    private String buildMethodNameByText(String text) {
        String methodName;
        if (text.length() > 1) {
            methodName = text.substring(0, 1).toUpperCase() + text.substring(1);
        } else {
            methodName = text.toUpperCase();
        }
        return methodName;
    }

    private Object tryGetFieldByCache(Object evaluate) {
        Object field = getCachedMember();
        if (field != null) {
            try {
                return JavaReflection.getInstance().getFieldValue(evaluate, field);
            } catch (RuntimeException t) {
                // fall through
            }
        }
        return FAIL_FLAG;
    }

    private Object tryGetFieldByReflection(Object evaluate, String text) {
        Field field = JavaReflection.getInstance().getField(evaluate, text);
        if (field == null) {
            return FAIL_FLAG;
        }
        setCachedMember(field);
        return JavaReflection.getInstance().getFieldValue(evaluate, field);
    }
}